import React, { PropsWithChildren, createContext, useCallback, useContext, useEffect, useReducer, useState } from "react";
export enum StoreStatus {
  Idle = "idle",
  Initialized = "loaded",
  Initializing = "loading",
}
export type AnyAction = {
  type: string;
  payload?: any;
}
export type UseStoreResult<Value, Action> = readonly [Value, React.Dispatch<Action>, StoreStatus, IStore<Value, Action>]
export interface IStore<Value, Action> {
  readonly Provider: (props: PropsWithChildren) => JSX.Element;
  readonly useStore: () => UseStoreResult<Value, Action>;
  readonly value: Value;
  readonly dispatch: React.Dispatch<Action>
}
class Store<Value, Action> implements IStore<Value, Action> {
  readonly name: string;
  status: StoreStatus = StoreStatus.Idle;
  get Provider() { return this._Provider! }
  get useStore() { return this._useStore! }
  get value() { return this._value! }
  set value(v) { this._value = v }
  get dispatch() { return this._dispatch! }
  set dispatch(v) { this._dispatch = v }
  private _Provider?: (props: PropsWithChildren) => JSX.Element;
  private _useStore?: () => UseStoreResult<Value, Action>;
  private _value?: Value;
  private _dispatch?: React.Dispatch<Action>
  constructor(name: string) {
    this.name = name
  }
  init(
    Provider: (props: PropsWithChildren) => JSX.Element,
    useStore: () => UseStoreResult<Value, Action>,
    value: Value,
  ): this {
    this._Provider = Provider;
    this._useStore = useStore;
    this._value = value;
    return this;
  }


}
export interface Reducer<Value, Actions> {
  (state: Value, actions: Actions): Value | Promise<Value>
}
export interface Initer<Value> {
  (): Promise<Partial<Value>> | Partial<Value>
}
export interface Dispatch<Actions> {
  (actions: Actions): void
}
export function makeStore<
  Value extends NonNullable<{}> = NonNullable<{}>,
  Action extends AnyAction = AnyAction
>(
  name: string,
  default_value: Value,
  reducer: Reducer<Value, Action>,
  initer?: Initer<Value>
): IStore<Value, Action> {
  let store = new Store<Value, Action>(name);
  store.status = initer ? StoreStatus.Idle : StoreStatus.Initialized;
  let _actions: Action[] = []
  const context = createContext<UseStoreResult<Value, Action>>([default_value, async () => { }, store.status, store]);
  const _reducer = (state = default_value, action: Action) => {
    switch (action.type) {
      case '__merge':
      case '__reset':
        return { ...action.payload };
      default:
        console.warn("_reducer fall through!", action)
    }
  };
  interface IProviderProps extends PropsWithChildren {
    showAnyway?: boolean;
  }
  function Provider(props: IProviderProps): JSX.Element {
    const { children, showAnyway = false } = props;
    const [state, _dispatch] = useReducer(_reducer, default_value);
    const [status, set_status] = useState(store.status)
    const dispatch = useCallback(async (action: Action) => {
      _actions.push(action);
      if (_actions.length > 1 && store.status === 'loaded')
        return;
      while (true) {
        const action = _actions[0]
        if (!action) break;
        const prev_value = store.value
        const next_value = store.value = await reducer(prev_value, action)
        _dispatch({ type: '__reset', payload: next_value } as any)
        _actions.shift()
      }
    }, [_dispatch])

    store.dispatch = dispatch;

    useEffect(() => {
      if (store.status !== StoreStatus.Idle)
        return;
      if (!initer) {
        set_status(store.status = StoreStatus.Initialized)
        return;
      }
      const payload = initer()
      if (!is_promise(payload)) {
        set_status(store.status = StoreStatus.Initialized)
        _dispatch({ type: '__reset', payload: store.value = { ...default_value, ...payload } } as any);
        return;
      }
      set_status(store.status = StoreStatus.Initializing)
      payload.then((payload) => {
        set_status(store.status = StoreStatus.Initialized)
        _dispatch({ type: '__reset', payload: store.value = { ...default_value, ...payload } } as any)
      })
    }, [])

    if (status !== 'loaded' && !showAnyway) return <></>
    return (
      <context.Provider value={[state, dispatch, status, store]}>
        {children}
      </context.Provider>
    );
  }
  const useStore = () => useContext(context)
  return store.init(Provider, useStore, default_value);
}

function is_promise<V>(value: any): value is Promise<V> {
  return !!value
    && (typeof value === 'object' || typeof value === 'function')
    && typeof value.then === 'function';
}